/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：呼叫中心-网关层
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taiping.cc.gateway.security.handler;

import com.taiping.cc.gateway.app.properties.GlobalBizProperty;
import com.taiping.cc.gateway.security.config.CustomizeSecurityProperties;
import com.taipingframework.boot.web.response.status.ApiStatusEnum;
import com.taipingframework.utility.constant.ApplicationConstant;
import com.taipingframework.utility.constant.SystemConstant;
import com.taipingframework.utility.exception.ApplicationException;
import com.taipingframework.utility.exception.SystemInterrupterException;
import com.taipingframework.utility.thread.ParallelHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.util.AntPathMatcher;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;

/**
 * 用户权限鉴权处理，判断用户是否拥有访问API资源的权限
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
//CHECKSTYLE:OFF

    private final CustomizeSecurityProperties properties;
    private final GlobalBizProperty globalBizProperty;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
        return authentication.filter(Authentication::isAuthenticated)
                .switchIfEmpty(Mono.error(new SystemInterrupterException(ApiStatusEnum.ERR_A0312)))
                .map((auth) -> authorityValidate(auth, authorizationContext))
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false))
                .onErrorResume(e -> Mono.error(new ApplicationException(e)));
    }

    /**
     * API鉴权
     */
    private boolean authorityValidate(Authentication authentication, AuthorizationContext authorizationContext) throws ApplicationException {
        log.debug("动态感知业务配置的变更：{}", globalBizProperty.toString());
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();

        String clientRequestId = Optional.ofNullable(request.getHeaders().getFirst(ApplicationConstant.RESPONSE_IDENTITY))
                .orElse("XXXXXXXXXXXXXXXXXXXXXXXX");
        String logId = String.format("[%s]", clientRequestId);

        try {
            // 获取后端API在网关层的资源路径(通过截取的方式移除第一个路径分隔符)
            String path = request.getURI().getPath().substring(1);
            // 此处需要把网关的前缀去掉，后期可能需要优化，配置资源路径为网关的路径
            String newPath = Arrays.stream(StringUtils.tokenizeToStringArray(path, SystemConstant.FOLDER_SEPARATOR))
                    .skip(2)
                    .collect(Collectors.joining(SystemConstant.FOLDER_SEPARATOR));
            newPath = SystemConstant.FOLDER_SEPARATOR + newPath;
            // 去掉url请求参数
            if (newPath.contains(SystemConstant.QUESTION_MARK)) {
                newPath = newPath.split("\\?")[0];
            }

            //（1）通过已认证后不需要鉴权的名单
            List<String> noNeedAuthentication = properties.getNoNeedAuthentication();
            // 进行路径匹配，如果匹配成功则放行
            if (matchingPath(noNeedAuthentication, newPath, logId)) {
                return true;
            }

            //（2）获取用户的角色列表
            List<String> roleCodeList = new ArrayList<>();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                String roleCode = authority.getAuthority();
                roleCodeList.add(roleCode);
            }

            //（3）根据用户的角色读取缓存，获取API-Path列表
            CopyOnWriteArraySet<String> authorizationList = readApiPathListWithRole(roleCodeList);

            //（4）进行路径匹配，如果匹配成功放行，失败则返回false
            return matchingPath(authorizationList, newPath, logId);
        } catch (Exception e) {
            log.error(logId + "权限认证失败：{}", e.getMessage());
            throw new ApplicationException(e);
        }
    }

    /**
     * 对API资源路径进行匹配
     *
     * @param pathList 路径列表
     * @param path     处理后的API访问路径
     * @return 是否匹配成功
     */
    private boolean matchingPath(@NonNull Collection<String> pathList, @NonNull String path, String logId) {
        for (String pathCache : pathList) {
            if (!StringUtils.isEmpty(pathCache) && antPathMatcher.match(pathCache, path)) {
                log.info(logId + "用户请求API校验通过，GrantedAuthority：{} Path：{}", pathCache, path);
                return true;
            }
        }
        return false;
    }

    /**
     * 根据角色读取授权API-Path列表
     */
    private CopyOnWriteArraySet<String> readApiPathListWithRole(List<String> roleCodeList) {
        CopyOnWriteArraySet<String> authorizationList = new CopyOnWriteArraySet<>();
        // TODO - 根据roleCode读取缓存中api-path列表
        ParallelHelper.parallelHandle(roleCodeList, (roleCode) -> {
            /* 根据上方的web-api-path通配符匹配逻辑，可知：
             * @RequestMapping("/account") 与 @RequestMapping("/api/dlt/account") 是等价的；
             * @RequestMapping("/security") 与 @RequestMapping("/api/dlt/security") 是等价的；
             *
             * 当直接调用`登录通`的接口时，为了适配上方的web-api-path通配符匹配逻辑，则要求`登录通`
             * 模块的web-api在路径上需要保持一致，即注解@RequestMapping的参数需要额外多出`/api/dlt`的部分！
             *
             * 另外需要注意，在当前项目中，必须保证每个web-api模块中 注解@RequestMapping 的参数path必须是唯一的，
             * 否则类似`authorizationList.add("/account/**")`的编码逻辑在执行权限控制时可能将导致bug！
             */

            // 此处硬编码意味着为所有登录用户开放访问权限
            authorizationList.add("/account/disable"); // 禁用账户的接口暂时开放给登录用户
            authorizationList.add("/account/push"); // 推送账户数据的接口暂时开放给登录用户
            authorizationList.add("/index/**"); // 开放模块cc-platform-web-api中IndexController下的所有接口
        });
        return authorizationList;
    }

//CHECKSTYLE:ON
}
